diff --git a/core/modules/media/media.permissions.yml b/core/modules/media/media.permissions.yml
index 530365e1f4..79eadb5058 100644
--- a/core/modules/media/media.permissions.yml
+++ b/core/modules/media/media.permissions.yml
@@ -23,3 +23,7 @@ delete any media:
 
 create media:
   title: 'Create media'
+
+view all media revisions:
+  title: 'View all media revisions'
+  description: 'To view a revision, you also need permission to view the media item.'
diff --git a/core/modules/media/media.routing.yml b/core/modules/media/media.routing.yml
index 67ea1fb81c..c87fcad9ea 100644
--- a/core/modules/media/media.routing.yml
+++ b/core/modules/media/media.routing.yml
@@ -4,3 +4,12 @@ entity.media.multiple_delete_confirm:
     _form: '\Drupal\media\Form\MediaDeleteMultipleConfirmForm'
   requirements:
     _permission: 'administer media+delete any media'
+
+entity.media.revision:
+  path: '/media/{media}/revisions/{media_revision}/view'
+  defaults:
+    _controller: '\Drupal\media\Controller\MediaController::revisionShow'
+    _title_callback: '\Drupal\media\Controller\MediaController::revisionPageTitle'
+  requirements:
+    _access_media_revision: 'view'
+    media: \d+
diff --git a/core/modules/media/media.services.yml b/core/modules/media/media.services.yml
index a286b528d9..f22f90a124 100644
--- a/core/modules/media/media.services.yml
+++ b/core/modules/media/media.services.yml
@@ -2,3 +2,9 @@ services:
   plugin.manager.media.source:
     class: Drupal\media\MediaSourceManager
     parent: default_plugin_manager
+
+  access_check.media.revision:
+    class: Drupal\media\Access\MediaRevisionAccessCheck
+    arguments: ['@entity_type.manager']
+    tags:
+      - { name: access_check, applies_to: _access_media_revision }
diff --git a/core/modules/media/src/Access/MediaRevisionAccessCheck.php b/core/modules/media/src/Access/MediaRevisionAccessCheck.php
new file mode 100644
index 0000000000..35228a8b18
--- /dev/null
+++ b/core/modules/media/src/Access/MediaRevisionAccessCheck.php
@@ -0,0 +1,135 @@
+<?php
+
+namespace Drupal\media\Access;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Routing\Access\AccessInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\media\MediaInterface;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Provides an access checker for media item revisions.
+ *
+ * @ingroup media_access
+ */
+class MediaRevisionAccessCheck implements AccessInterface {
+
+  /**
+   * The media storage.
+   *
+   * @var \Drupal\media\MediaStorageInterface
+   */
+  protected $mediaStorage;
+
+  /**
+   * The media access control handler.
+   *
+   * @var \Drupal\Core\Entity\EntityAccessControlHandlerInterface
+   */
+  protected $mediaAccess;
+
+  /**
+   * A static cache of access checks.
+   *
+   * @var array
+   */
+  protected $access = [];
+
+  /**
+   * Constructs a new MediaRevisionAccessCheck.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->mediaStorage = $entity_type_manager->getStorage('media');
+    $this->mediaAccess = $entity_type_manager->getAccessControlHandler('media');
+  }
+
+  /**
+   * Checks routing access for the media item revision.
+   *
+   * @param \Symfony\Component\Routing\Route $route
+   *   The route to check against.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The currently logged in account.
+   * @param int $media_revision
+   *   (optional) The media item revision ID. If not specified, but $media is,
+   *   access is checked for that object's revision.
+   * @param \Drupal\media\MediaInterface $media
+   *   (optional) A media item. Used for checking access to a media items
+   *   default revision when $media_revision is unspecified. Ignored when
+   *   $media_revision is specified. If neither $media_revision nor $media are
+   *   specified, then access is denied.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  public function access(Route $route, AccountInterface $account, $media_revision = NULL, MediaInterface $media = NULL) {
+    if ($media_revision) {
+      $media = $this->mediaStorage->loadRevision($media_revision);
+    }
+    $operation = $route->getRequirement('_access_media_revision');
+    return AccessResult::allowedIf($media && $this->checkAccess($media, $account, $operation))->cachePerPermissions()->addCacheableDependency($media);
+  }
+
+  /**
+   * Checks media item revision access.
+   *
+   * @param \Drupal\media\MediaInterface $media
+   *   The media item to check.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   A user object representing the user for whom the operation is to be
+   *   performed.
+   * @param string $op
+   *   (optional) The specific operation being checked. Defaults to 'view'.
+   *
+   * @return bool
+   *   TRUE if the operation may be performed, FALSE otherwise.
+   */
+  public function checkAccess(MediaInterface $media, AccountInterface $account, $op = 'view') {
+    $map = [
+      'view' => 'view all media revisions',
+    ];
+
+    if (!$media || !isset($map[$op])) {
+      // If there was no media to check against, or the $op was not one of the
+      // supported ones, we return access denied.
+      return FALSE;
+    }
+
+    // Statically cache access by revision ID, language code, user account ID,
+    // and operation.
+    $langcode = $media->language()->getId();
+    $cid = $media->getRevisionId() . ':' . $langcode . ':' . $account->id() . ':' . $op;
+
+    if (!isset($this->access[$cid])) {
+      // Perform basic permission checks first.
+      if (!$account->hasPermission($map[$op]) && !$account->hasPermission('administer media')) {
+        $this->access[$cid] = FALSE;
+        return FALSE;
+      }
+
+      // There should be at least two revisions. If the vid of the given media
+      // item and the vid of the default revision differ, then we already have
+      // two different revisions so there is no need for a separate database
+      // check.
+      if ($media->isDefaultRevision() && ($this->mediaStorage->countDefaultLanguageRevisions($media) == 1)) {
+        $this->access[$cid] = FALSE;
+      }
+      elseif ($account->hasPermission('administer media')) {
+        $this->access[$cid] = TRUE;
+      }
+      else {
+        // First check the access to the default revision and finally, if the
+        // media passed in is not the default revision then access to that, too.
+        $this->access[$cid] = $this->mediaAccess->access($this->mediaStorage->load($media->id()), $op, $account) && ($media->isDefaultRevision() || $this->mediaAccess->access($media, $op, $account));
+      }
+    }
+
+    return $this->access[$cid];
+  }
+
+}
diff --git a/core/modules/media/src/Controller/MediaController.php b/core/modules/media/src/Controller/MediaController.php
new file mode 100644
index 0000000000..7d500ffce0
--- /dev/null
+++ b/core/modules/media/src/Controller/MediaController.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace Drupal\media\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Datetime\DateFormatterInterface;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\Controller\EntityViewController;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Render\RendererInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Returns responses for media routes.
+ */
+class MediaController extends ControllerBase implements ContainerInjectionInterface {
+
+  /**
+   * The entity manager.
+   *
+   * @var \Drupal\Core\Entity\EntityManagerInterface
+   */
+  protected $entityManager;
+
+  /**
+   * The date formatter service.
+   *
+   * @var \Drupal\Core\Datetime\DateFormatterInterface
+   */
+  protected $dateFormatter;
+
+  /**
+   * The renderer service.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * Constructs a MediaController object.
+   *
+   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+   *   The entity manager.
+   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
+   *   The date formatter service.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer service.
+   */
+  public function __construct(EntityManagerInterface $entity_manager, DateFormatterInterface $date_formatter, RendererInterface $renderer) {
+    $this->entityManager = $entity_manager;
+    $this->dateFormatter = $date_formatter;
+    $this->renderer = $renderer;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity.manager'),
+      $container->get('date.formatter'),
+      $container->get('renderer')
+    );
+  }
+
+  /**
+   * Displays a media item revision.
+   *
+   * @param int $media_revision
+   *   The media item revision ID.
+   *
+   * @return array
+   *   An array suitable for drupal_render().
+   */
+  public function revisionShow($media_revision) {
+    $media = $this->entityManager->getStorage('media')->loadRevision($media_revision);
+    $media = $this->entityManager->getTranslationFromContext($media);
+    $media_view_controller = new EntityViewController($this->entityManager, $this->renderer);
+    $page = $media_view_controller->view($media);
+    unset($page['media'][$media->id()]['#cache']);
+    return $page;
+  }
+
+  /**
+   * Page title callback for a media revision.
+   *
+   * @param int $media_revision
+   *   The media revision ID.
+   *
+   * @return string
+   *   The page title.
+   */
+  public function revisionPageTitle($media_revision) {
+    $media = $this->entityTypeManager->getStorage('media')->loadRevision($media_revision);
+    return $this->t('Revision of %title from %date', ['%title' => $media->label(), '%date' => $this->dateFormatter->format($media->getRevisionCreationTime())]);
+  }
+
+}
diff --git a/core/modules/media/src/Entity/Media.php b/core/modules/media/src/Entity/Media.php
index 858404112e..f8713f8f56 100644
--- a/core/modules/media/src/Entity/Media.php
+++ b/core/modules/media/src/Entity/Media.php
@@ -29,7 +29,7 @@
  *   ),
  *   bundle_label = @Translation("Media type"),
  *   handlers = {
- *     "storage" = "Drupal\Core\Entity\Sql\SqlContentEntityStorage",
+ *     "storage" = "Drupal\media\MediaStorage",
  *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
  *     "list_builder" = "Drupal\Core\Entity\EntityListBuilder",
  *     "access" = "Drupal\media\MediaAccessControlHandler",
@@ -76,6 +76,7 @@
  *     "canonical" = "/media/{media}",
  *     "delete-form" = "/media/{media}/delete",
  *     "edit-form" = "/media/{media}/edit",
+ *     "revision" = "/media/{media}/revisions/{media_revision}/view",
  *     "admin-form" = "/admin/structure/media/manage/{media_type}"
  *   }
  * )
diff --git a/core/modules/media/src/MediaStorage.php b/core/modules/media/src/MediaStorage.php
new file mode 100644
index 0000000000..2f7aac2e20
--- /dev/null
+++ b/core/modules/media/src/MediaStorage.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Drupal\media;
+
+use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
+
+/**
+ * Defines the storage handler class for media items.
+ *
+ * This extends the base storage class, adding required special handling for
+ * media items.
+ */
+class MediaStorage extends SqlContentEntityStorage implements MediaStorageInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function countDefaultLanguageRevisions(MediaInterface $media) {
+    return $this->database->query('SELECT COUNT(*) FROM {media_field_revision} WHERE mid = :mid AND default_langcode = 1', [':mid' => $media->id()])->fetchField();
+  }
+
+}
diff --git a/core/modules/media/src/MediaStorageInterface.php b/core/modules/media/src/MediaStorageInterface.php
new file mode 100644
index 0000000000..33adebde7f
--- /dev/null
+++ b/core/modules/media/src/MediaStorageInterface.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Drupal\media;
+
+use Drupal\Core\Entity\ContentEntityStorageInterface;
+
+/**
+ * Defines an interface for media item storage classes.
+ */
+interface MediaStorageInterface extends ContentEntityStorageInterface {
+
+  /**
+   * Counts the number of revisions in the default language.
+   *
+   * @param \Drupal\media\MediaInterface $media
+   *   The media item.
+   *
+   * @return int
+   *   The number of revisions in the default language.
+   */
+  public function countDefaultLanguageRevisions(MediaInterface $media);
+
+}
diff --git a/core/modules/media/tests/src/Functional/MediaRevisionTest.php b/core/modules/media/tests/src/Functional/MediaRevisionTest.php
index ce1897f3ce..18bd08100e 100644
--- a/core/modules/media/tests/src/Functional/MediaRevisionTest.php
+++ b/core/modules/media/tests/src/Functional/MediaRevisionTest.php
@@ -43,6 +43,11 @@ public function testFileMediaRevision() {
     $page->fillField('Name', 'Foobaz');
     $page->pressButton('Save and keep published');
     $this->assertRevisionCount($media, 2);
+
+    // Confirm the correct revision title appears on "view revisions" page.
+    $media = $this->container->get('entity_type.manager')->getStorage('media')->load(1);
+    $this->drupalGet("media/" . $media->id() . "/revisions/" . $media->getRevisionId() . "/view");
+    $assert->pageTextContains('Foobaz');
   }
 
   /**
@@ -83,6 +88,11 @@ public function testImageMediaRevision() {
     $page->fillField('Name', 'Foobaz');
     $page->pressButton('Save and keep published');
     $this->assertRevisionCount($media, 2);
+
+    // Confirm the correct revision title appears on "view revisions" page.
+    $media = $this->container->get('entity_type.manager')->getStorage('media')->load(1);
+    $this->drupalGet("media/" . $media->id() . "/revisions/" . $media->getRevisionId() . "/view");
+    $assert->pageTextContains('Foobaz');
   }
 
   /**
