diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
index 1172406e34..ef9d24f493 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
@@ -809,6 +809,17 @@ public function save(EntityInterface $entity) {
   /**
    * {@inheritdoc}
    */
+  public function countDefaultLanguageRevisions(EntityInterface $entity) {
+    $query = $this->database->select($this->revisionDataTable);
+    $query->addExpression('COUNT(*)');
+    $query->condition($this->idKey, $entity->id());
+    $query->condition('default_langcode', 1);
+    return $query->execute()->fetchField();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) {
     $full_save = empty($names);
     $update = !$full_save || !$entity->isNew();
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlEntityStorageInterface.php b/core/lib/Drupal/Core/Entity/Sql/SqlEntityStorageInterface.php
index b97e0fdfd4..fd5b3378c3 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlEntityStorageInterface.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlEntityStorageInterface.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Core\Entity\Sql;
 
+use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityStorageInterface;
 
 /**
@@ -21,4 +22,15 @@
    */
   public function getTableMapping(array $storage_definitions = NULL);
 
+  /**
+   * Counts the number of revisions in the default language.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity for which to to count the revisions.
+   *
+   * @return int
+   *   The number of revisions in the default language.
+   */
+  public function countDefaultLanguageRevisions(EntityInterface $entity);
+
 }
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..9ccae7c6cd
--- /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\Core\Entity\ContentEntityStorageInterface
+   */
+  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..bdb089ea9c 100644
--- a/core/modules/media/src/Entity/Media.php
+++ b/core/modules/media/src/Entity/Media.php
@@ -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/tests/src/Functional/MediaRevisionTest.php b/core/modules/media/tests/src/Functional/MediaRevisionTest.php
index ce1897f3ce..a22050e552 100644
--- a/core/modules/media/tests/src/Functional/MediaRevisionTest.php
+++ b/core/modules/media/tests/src/Functional/MediaRevisionTest.php
@@ -4,6 +4,9 @@
 
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\field\Entity\FieldConfig;
+use Drupal\media\MediaInterface;
+use Drupal\user\Entity\Role;
+use Drupal\user\RoleInterface;
 
 /**
  * Tests the revisionability of media entities.
@@ -13,6 +16,62 @@
 class MediaRevisionTest extends MediaFunctionalTestBase {
 
   /**
+   * Checks media revision operations.
+   */
+  public function testRevisions() {
+    $assert = $this->assertSession();
+
+    /** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $media_storage */
+    $media_storage = $this->container->get('entity.manager')->getStorage('media');
+
+    // Create a media type and media item.
+    $media_type = $this->createMediaType();
+    $media = $media_storage->create([
+      'bundle' => $media_type->id(),
+      'name' => 'Unnamed',
+    ]);
+    $media->save();
+
+    // You can't access the revision page when there is only 1 revision.
+    $this->drupalGet('media/' . $media->id() . '/revisions/' . $media->getRevisionId() . '/view');
+    $assert->statusCodeEquals(403);
+
+    // Create some revisions.
+    $media_revisions = [];
+    $media_revisions[] = clone $media;
+    $revision_count = 3;
+    for ($i = 0; $i < $revision_count; $i++) {
+      $media->revision_log = $this->randomMachineName(32);
+      $media = $this->createMediaRevision($media);
+      $media_revisions[] = clone $media;
+    }
+
+    // Get the last revision for simple checks.
+    /** @var \Drupal\media\MediaInterface $media */
+    $media = end($media_revisions);
+
+    // Test permissions.
+    $this->drupalLogin($this->nonAdminUser);
+    /** @var \Drupal\user\RoleInterface $role */
+    $role = Role::load(RoleInterface::AUTHENTICATED_ID);
+
+    // Test 'view all media revisions' permission ('view media' permission is
+    // needed as well).
+    user_role_revoke_permissions($role->id(), ['view media', 'view all media revisions']);
+    $this->drupalGet('media/' . $media->id() . '/revisions/' . $media->getRevisionId() . '/view');
+    $assert->statusCodeEquals(403);
+    $this->grantPermissions($role, ['view media', 'view all media revisions']);
+    $this->drupalGet('media/' . $media->id() . '/revisions/' . $media->getRevisionId() . '/view');
+    $assert->statusCodeEquals(200);
+
+    // Confirm the revision page shows the correct title.
+    $assert->pageTextContains($media->label());
+
+    // Confirm that the last revision is the default revision.
+    $this->assertTrue($media->isDefaultRevision(), 'Last revision is the default.');
+  }
+
+  /**
    * Tests creating revisions of a File media item.
    */
   public function testFileMediaRevision() {
@@ -43,6 +102,13 @@ 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')
+      ->loadUnchanged(1);
+    $this->drupalGet("media/" . $media->id() . "/revisions/" . $media->getRevisionId() . "/view");
+    $assert->pageTextContains('Foobaz');
   }
 
   /**
@@ -83,6 +149,29 @@ 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')
+      ->loadUnchanged(1);
+    $this->drupalGet("media/" . $media->id() . "/revisions/" . $media->getRevisionId() . "/view");
+    $assert->pageTextContains('Foobaz');
+  }
+
+  /**
+   * Creates a new revision for a given media item.
+   *
+   * @param \Drupal\media\MediaInterface $media
+   *   A media object.
+   *
+   * @return \Drupal\media\MediaInterface
+   *   A media object with up to date revision information.
+   */
+  protected function createMediaRevision(MediaInterface $media) {
+    $media->set('name', $this->randomMachineName());
+    $media->setNewRevision();
+    $media->save();
+    return $media;
   }
 
   /**
diff --git a/core/modules/node/src/NodeStorage.php b/core/modules/node/src/NodeStorage.php
index af85a067d9..459e85947b 100644
--- a/core/modules/node/src/NodeStorage.php
+++ b/core/modules/node/src/NodeStorage.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\node;
 
+use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Language\LanguageInterface;
@@ -37,8 +38,8 @@ public function userRevisionIds(AccountInterface $account) {
   /**
    * {@inheritdoc}
    */
-  public function countDefaultLanguageRevisions(NodeInterface $node) {
-    return $this->database->query('SELECT COUNT(*) FROM {node_field_revision} WHERE nid = :nid AND default_langcode = 1', [':nid' => $node->id()])->fetchField();
+  public function countDefaultLanguageRevisions(EntityInterface $entity) {
+    return $this->database->query('SELECT COUNT(*) FROM {node_field_revision} WHERE nid = :nid AND default_langcode = 1', [':nid' => $entity->id()])->fetchField();
   }
 
   /**
diff --git a/core/modules/node/src/NodeStorageInterface.php b/core/modules/node/src/NodeStorageInterface.php
index 19668c2084..ffc1a71aeb 100644
--- a/core/modules/node/src/NodeStorageInterface.php
+++ b/core/modules/node/src/NodeStorageInterface.php
@@ -34,17 +34,6 @@ public function revisionIds(NodeInterface $node);
   public function userRevisionIds(AccountInterface $account);
 
   /**
-   * Counts the number of revisions in the default language.
-   *
-   * @param \Drupal\node\NodeInterface $node
-   *   The node entity.
-   *
-   * @return int
-   *   The number of revisions in the default language.
-   */
-  public function countDefaultLanguageRevisions(NodeInterface $node);
-
-  /**
    * Updates all nodes of one type to be of another type.
    *
    * @param string $old_type
