diff --git a/src/Controller/RevisionControllerTrait.php b/src/Controller/RevisionControllerTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..c77e793a9eddd1e504bb52699b3ec360aee99f79
--- /dev/null
+++ b/src/Controller/RevisionControllerTrait.php
@@ -0,0 +1,209 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\entity\Controller\RevisionControllerTrait.
+ */
+
+namespace Drupal\entity\Controller;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Defines a trait for common revision UI functionality.
+ */
+trait RevisionControllerTrait {
+
+  use StringTranslationTrait;
+
+  /**
+   * @return \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  abstract protected function entityTypeManager();
+
+  /**
+   * @return \Drupal\Core\Language\LanguageManagerInterface
+   */
+  public abstract function languageManager();
+
+  /**
+   * Determines if the user has permission to revert revisions.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity to check revert access for.
+   *
+   * @return bool
+   *   TRUE if the user has revert access.
+   */
+  abstract protected function hasRevertRevisionAccess(EntityInterface $entity);
+
+  /**
+   * Determines if the user has permission to delete revisions.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity to check delete revision access for.
+   *
+   * @return bool
+   *   TRUE if the user has delete revision access.
+   */
+  abstract protected function hasDeleteRevisionAccess(EntityInterface $entity);
+
+  /**
+   * Builds a link to revert an entity revision.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity_revision
+   *   The entity to build a revert revision link for.
+   *
+   * @return array A link render array.
+   * A link render array.
+   * @internal param int $revision_id The revision ID of the revert link.*   The revision ID of the revert link.
+   *
+   */
+  abstract protected function buildRevertRevisionLink(EntityInterface $entity_revision);
+
+  /**
+   * Builds a link to delete an entity revision.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity_revision
+   *   The entity to build a delete revision link for.
+   *
+   * @return array A link render array.
+   * A link render array.
+   * @internal param int $revision_id The revision ID of the delete link.*   The revision ID of the delete link.
+   *
+   */
+  abstract protected function buildDeleteRevisionLink(EntityInterface $entity_revision);
+
+  /**
+   * Returns a string providing details of the revision.
+   *
+   * E.g. Node describes its revisions using {date} by {username}. For the
+   *   non-current revision, it also provides a link to view that revision.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $revision
+   * @param bool $is_current
+   *   TRUE if the revision is the current revision.
+   *
+   * @return string
+   *   Returns a string to provide the details of the revision.
+   */
+  abstract protected function getRevisionDescription(ContentEntityInterface $revision, $is_current = FALSE);
+
+  /**
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *
+   * @return array
+   */
+  protected function revisionIds(ContentEntityInterface $entity) {
+    $entity_type = $entity->getEntityTypeId();
+    $result = $this->entityTypeManager()->getStorage($entity_type)->getQuery()
+      ->allRevisions()
+      ->condition($entity_type->getKey('id'), $entity->id())
+      ->sort($entity_type->getKey('revision'), 'DESC')
+      ->execute();
+    return array_keys($result);
+  }
+
+  /**
+   * Generates an overview table of older revisions of an entity.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *   An entity object.
+   *
+   * @return array
+   *   An array as expected by drupal_render().
+   */
+  public function revisionOverview(ContentEntityInterface $entity) {
+    $langcode = $this->languageManager()
+      ->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)
+      ->getId();
+    /** @var \Drupal\content_entity_base\Entity\Storage\RevisionableStorageInterface $entity_storage */
+    $entity_storage = $this->entityTypeManager()
+      ->getStorage($entity->getEntityTypeId());
+
+    $header = [$this->t('Revision'), $this->t('Operations')];
+    $rows = [];
+
+    $revision_ids = $this->revisionIds($entity);
+    // @todo Expand the entity storage to load multiple revisions.
+    $entity_revisions = array_combine($revision_ids, array_map(function($vid) use ($entity_storage) {
+      return $entity_storage->loadRevision($vid);
+      }, $revision_ids));
+
+    $latest_revision = TRUE;
+
+    foreach ($entity_revisions as $revision) {
+      $row = [];
+      /** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
+      if ($revision->hasTranslation($langcode) && $revision->getTranslation($langcode)
+          ->isRevisionTranslationAffected()
+      ) {
+        if ($latest_revision) {
+          $row[] = $this->getRevisionDescription($revision, TRUE);
+          $row[] = [
+            'data' => [
+              '#prefix' => '<em>',
+              '#markup' => $this->t('Current revision'),
+              '#suffix' => '</em>',
+            ],
+          ];
+          foreach ($row as &$current) {
+            $current['class'] = ['revision-current'];
+          }
+          $latest_revision = FALSE;
+        }
+        else {
+          $row[] = $this->getRevisionDescription($revision, FALSE);
+          $links = $this->getOperationLinks($revision);
+
+          $row[] = [
+            'data' => [
+              '#type' => 'operations',
+              '#links' => $links,
+            ],
+          ];
+        }
+      }
+
+      $rows[] = $row;
+    }
+
+    $build[$entity->getEntityTypeId() . '_revisions_table'] = [
+      '#theme' => 'table',
+      '#rows' => $rows,
+      '#header' => $header,
+    ];
+
+    // We have no clue about caching yet.
+    $build['#cache']['max-age'] = 0;
+
+    return $build;
+  }
+
+  /**
+   * Get the links of the operations for an entity revision.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity_revision
+   *   The entity to build the revision links for.
+   *
+   * @return array
+   *   The operation links.
+   */
+  protected function getOperationLinks(EntityInterface $entity_revision) {
+    $links = [];
+    $revert_permission = $this->hasRevertRevisionAccess($entity_revision);
+    $delete_permission = $this->hasDeleteRevisionAccess($entity_revision);
+    if ($revert_permission) {
+      $links['revert'] = $this->buildRevertRevisionLink($entity_revision);
+    }
+
+    if ($delete_permission) {
+      $links['delete'] = $this->buildDeleteRevisionLink($entity_revision);
+    }
+    return $links;
+  }
+
+}
diff --git a/src/Controller/RevisionOverviewController.php b/src/Controller/RevisionOverviewController.php
new file mode 100644
index 0000000000000000000000000000000000000000..1285b7d920b396b3ab05d7bbe1a72085cb80bf1d
--- /dev/null
+++ b/src/Controller/RevisionOverviewController.php
@@ -0,0 +1,140 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\entity\Controller\RevisionOverviewController.
+ */
+
+namespace Drupal\entity\Controller;
+
+use Drupal\Component\Utility\Xss;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Datetime\DateFormatterInterface;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\entity\Revision\EntityRevisionLogInterface;
+use Drupal\user\EntityOwnerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+class RevisionOverviewController extends ControllerBase {
+
+  use RevisionControllerTrait;
+
+  /**
+   * The date formatter.
+   *
+   * @var \Drupal\Core\Datetime\DateFormatterInterface
+   */
+  protected $dateFormatter;
+
+  /**
+   * Creates a new RevisionOverviewController instance.
+   *
+   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
+   *   The date formatter.
+   */
+  public function __construct(DateFormatterInterface $date_formatter) {
+    $this->dateFormatter = $date_formatter;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static($container->get('date.formatter'));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function hasDeleteRevisionAccess(EntityInterface $entity) {
+    return $this->currentUser()->hasPermission("delete all {$entity->id()} revisions");
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildRevertRevisionLink(EntityInterface $entity_revision) {
+    return [
+      'title' => t('Revert'),
+      'url' => $entity_revision->toUrl('revision-revert'),
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildDeleteRevisionLink(EntityInterface $entity_revision) {
+    return [
+      'title' => t('Delete'),
+      'url' => $entity_revision->toUrl('revision-delete'),
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getRevisionDescription(ContentEntityInterface $revision, $is_current = FALSE) {
+    /** @var \Drupal\Core\Entity\ContentEntityInterface|\Drupal\user\EntityOwnerInterface|\Drupal\entity\Revision\EntityRevisionLogInterface $revision */
+
+    if ($revision instanceof EntityOwnerInterface) {
+      $username = [
+        '#theme' => 'username',
+        '#account' => $revision->getOwner(),
+      ];
+    }
+    else {
+      $username = '';
+    }
+
+    if ($revision instanceof EntityRevisionLogInterface) {
+      // Use revision link to link to revisions that are not active.
+      $date = $this->dateFormatter->format($revision->getRevisionCreationTime(), 'short');
+      if (!$is_current) {
+        $link = $revision->toLink($date, 'revision');
+      }
+      else {
+        $link = $revision->toLink($date);
+      }
+    }
+    else {
+      $link = $revision->toLink($revision->label(), 'revision');
+    }
+
+    $markup = '';
+    if ($revision instanceof EntityRevisionLogInterface) {
+      $markup = $revision->getRevisionLogMessage();
+    }
+
+    if ($username) {
+      $template = '{% trans %}{{ date }} by {{ username }}{% endtrans %}{% if message %}<p class="revision-log">{{ message }}</p>{% endif %}';
+    }
+    else {
+      $template = '{% trans %} {{ date }} {% endtrans %}{% if message %}<p class="revision-log">{{ message }}</p>{% endif %}';
+    }
+
+    $column = [
+      'data' => [
+        '#type' => 'inline_template',
+        '#template' => $template,
+        '#context' => [
+          'date' => $link,
+          'username' => $username,
+          'message' => ['#markup' => $markup, '#allowed_tags' => Xss::getHtmlTagList()],
+        ],
+      ],
+    ];
+    return $column;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function hasRevertRevisionAccess(EntityInterface $entity) {
+    return AccessResult::allowedIfHasPermission($this->currentUser(), "revert all {$entity->getEntityTypeId()} revisions")->orIf(
+      AccessResult::allowedIfHasPermission($this->currentUser(), "revert {$entity->bundle()} {$entity->getEntityTypeId()} revisions")
+    );
+  }
+
+}
diff --git a/src/Routing/RevisionRouteProvider.php b/src/Routing/RevisionRouteProvider.php
index d539c6150b6083964ad09fcb87f688ff1f2b223b..870ece48f78d02099eeddffacd0891bf97a27818 100644
--- a/src/Routing/RevisionRouteProvider.php
+++ b/src/Routing/RevisionRouteProvider.php
@@ -30,6 +30,10 @@ class RevisionRouteProvider implements EntityRouteProviderInterface {
       $collection->add("entity.$entity_type_id.revision_revert_form", $view_route);
     }
 
+    if ($view_route = $this->getRevisionHistoryRoute($entity_type)) {
+      $collection->add("entity.$entity_type_id.version_history", $view_route);
+    }
+
     return $collection;
   }
 
@@ -97,4 +101,33 @@ class RevisionRouteProvider implements EntityRouteProviderInterface {
     }
   }
 
+  /**
+   * Gets the entity revision version history route.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return \Symfony\Component\Routing\Route|null
+   *   The generated route, if available.
+   */
+  protected function getRevisionHistoryRoute($entity_type) {
+    if ($entity_type->hasLinkTemplate('version-history')) {
+      $entity_type_id = $entity_type->id();
+      $route = new Route($entity_type->getLinkTemplate('revision'));
+      $route->addDefaults([
+        '_controller' => '\Drupal\entity\Controller\RevisionControllerTrait::revisionOverview',
+        '_title' => 'Revisions',
+      ]);
+      $route->addRequirements([
+        '_entity_access_revision' => "$entity_type_id.view",
+      ]);
+      $route->setOption('parameters', [
+        $entity_type->id() => [
+          'type' => 'entity:' . $entity_type->id(),
+        ],
+      ]);
+      return $route;
+    }
+  }
+
 }
diff --git a/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php b/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php
index d49e0bdcedfc0ca2842ad47eceb773446b0811c6..53c7ae441dc862b1ad97bd6f13c5dd6c4691954e 100644
--- a/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php
+++ b/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php
@@ -51,8 +51,9 @@ use Drupal\entity\Revision\RevisionableContentEntityBase;
  *     "canonical" = "/entity_test_enhanced/{entity_test_enhanced}",
  *     "revision" = "/entity_test_enhanced/{entity_test_enhanced}/revisions/{entity_test_enhanced_revision}/view",
  *     "revision-revert-form" = "/entity_test_enhanced/{entity_test_enhanced}/revisions/{entity_test_enhanced_revision}/revert",
+ *     "version-history" = "/entity_test_enhanced/{entity_test_enhanced}/revisions",
  *   },
- *   bundle_entity_type = "entity_test_enhanced_bundle"
+ *   bundle_entity_type = "entity_test_enhanced_bundle",
  * )
  */
 class EnhancedEntity extends RevisionableContentEntityBase {
diff --git a/tests/src/Kernel/RevisionBasicUITest.php b/tests/src/Kernel/RevisionBasicUITest.php
index efb6f75ddb43ed663b3bd3f5adf5478354962ce6..48ad0dd3b29d404155d48c7e528fad0181bc5102 100644
--- a/tests/src/Kernel/RevisionBasicUITest.php
+++ b/tests/src/Kernel/RevisionBasicUITest.php
@@ -43,6 +43,39 @@ class RevisionBasicUITest extends KernelTestBase {
     \Drupal::service('router.builder')->rebuild();
   }
 
+  public function testRevisionHistory() {
+    $entity = EnhancedEntity::create([
+      'name' => 'rev 1',
+    ]);
+    $entity->save();
+
+    $revision = clone $entity;
+    $revision->name->value = 'rev 2';
+    $revision->setNewRevision(TRUE);
+    $revision->isDefaultRevision(FALSE);
+    $revision->save();
+
+    $http_kernel = \Drupal::service('http_kernel');
+    $request = Request::create($revision->url('revision'));
+    $response = $http_kernel->handle($request);
+    $this->assertEquals(403, $response->getStatusCode());
+
+    $role = Role::create(['id' => 'test_role']);
+    $role->grantPermission('view all entity_test_enhanced revisions');
+    $role->grantPermission('administer entity_test_enhanced');
+    $role->save();
+
+    $user = User::create([
+      'name' => 'Test user',
+    ]);
+    $user->addRole($role->id());
+    \Drupal::service('account_switcher')->switchTo($user);
+
+    $request = Request::create($revision->url('revision'));
+    $response = $http_kernel->handle($request);
+    $this->assertEquals(200, $response->getStatusCode());
+  }
+
   public function testRevisionView() {
     $entity = EnhancedEntity::create([
       'name' => 'rev 1',