diff --git a/fillpdf.routing.yml b/fillpdf.routing.yml
index 9d12d1b5d1ef85293b575eb35b9714f98e63feab..fe16e9734e2c53a962a932a2a98ef571b352f9c0 100644
--- a/fillpdf.routing.yml
+++ b/fillpdf.routing.yml
@@ -62,6 +62,14 @@ entity.fillpdf_form.import_form:
   requirements:
     _entity_access: fillpdf_form.view
 
+entity.fillpdf_form.duplicate_form:
+  path: '/admin/structure/fillpdf/{fillpdf_form}/duplicate'
+  defaults:
+    _entity_form: fillpdf_form.duplicate
+    _title: 'Duplicate FillPDF form configuration and field mappings'
+  requirements:
+    _entity_access: fillpdf_form.duplicate
+
 entity.fillpdf_form_field.edit_form:
   path: '/admin/structure/fillpdf/{fillpdf_form}/{fillpdf_form_field}'
   defaults:
diff --git a/src/Entity/FillPdfForm.php b/src/Entity/FillPdfForm.php
index 1a8de655e73b37c2c47637e4a2680324c29c2efe..56765f874040ae3b4f63b679ea0ee7c59a51eead 100644
--- a/src/Entity/FillPdfForm.php
+++ b/src/Entity/FillPdfForm.php
@@ -23,6 +23,7 @@ use Drupal\fillpdf\Service\FillPdfAdminFormHelper;
  *     "form" = {
  *       "edit" = "Drupal\fillpdf\Form\FillPdfFormForm",
  *       "delete" = "Drupal\fillpdf\Form\FillPdfFormDeleteForm",
+ *       "duplicate" = "Drupal\fillpdf\Form\FillPdfFormDuplicateForm",
  *       "export" = "Drupal\fillpdf\Form\FillPdfFormExportForm",
  *       "import" = "Drupal\fillpdf\Form\FillPdfFormImportForm",
  *     },
@@ -40,6 +41,7 @@ use Drupal\fillpdf\Service\FillPdfAdminFormHelper;
  *     "canonical" = "/admin/structure/fillpdf/{fillpdf_form}",
  *     "edit-form" = "/admin/structure/fillpdf/{fillpdf_form}",
  *     "delete-form" = "/admin/structure/fillpdf/{fillpdf_form}/delete",
+ *     "duplicate-form" = "/admin/structure/fillpdf/{fillpdf_form}/duplicate",
  *     "export-form" = "/admin/structure/fillpdf/{fillpdf_form}/export",
  *     "import-form" = "/admin/structure/fillpdf/{fillpdf_form}/import",
  *     "collection" = "/admin/structure/fillpdf",
diff --git a/src/FillPdfFormAccessControlHandler.php b/src/FillPdfFormAccessControlHandler.php
index 8e5a32f9af5860e98c6b0ab9ae95fdab865beb2a..80e222265c491977885fa811967a027c858efd9e 100644
--- a/src/FillPdfFormAccessControlHandler.php
+++ b/src/FillPdfFormAccessControlHandler.php
@@ -21,6 +21,7 @@ class FillPdfFormAccessControlHandler extends EntityAccessControlHandler {
     switch ($operation) {
       case 'view':
       case 'update':
+      case 'duplicate':
       case 'delete':
         return AccessResult::allowedIfHasPermission($account, 'administer pdfs');
       default:
diff --git a/src/FillPdfFormListBuilder.php b/src/FillPdfFormListBuilder.php
index 5c972f327bf32e2d8b879a6acd8af6e01c5f9dc6..363e817046ff2292663c536d0c49bd1422dacd6a 100644
--- a/src/FillPdfFormListBuilder.php
+++ b/src/FillPdfFormListBuilder.php
@@ -22,6 +22,12 @@ class FillPdfFormListBuilder extends EntityListBuilder {
    * {@inheritdoc}
    */
   public function getDefaultOperations(EntityInterface $entity) {
+
+    $duplicate = [
+        'title' => t('Duplicate'),
+        'weight' => 10,
+        'url' => $this->ensureDestination($entity->toUrl('duplicate-form')),
+    ];
     $export = [
         'title' => t('Export configuration'),
         'weight' => 20,
@@ -34,6 +40,7 @@ class FillPdfFormListBuilder extends EntityListBuilder {
         ];
 
     $operations = parent::getDefaultOperations($entity) + [
+      'duplicate' => $duplicate,
       'export' => $export,
       'import' => $import,
     ];
diff --git a/src/Form/FillPdfFormDuplicateForm.php b/src/Form/FillPdfFormDuplicateForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..c38c13db544b9c9feebc2425d906601ff3bae2ed
--- /dev/null
+++ b/src/Form/FillPdfFormDuplicateForm.php
@@ -0,0 +1,124 @@
+<?php
+
+namespace Drupal\fillpdf\Form;
+
+use Drupal\Core\Entity\ContentEntityConfirmFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+
+/**
+ * Form controller for the FillPdfForm duplicate form.
+ *
+ * @internal
+ */
+class FillPdfFormDuplicateForm extends ContentEntityConfirmFormBase {
+
+  /**
+   * The FillPdfForm being duplicated.
+   *
+   * @var \Drupal\fillpdf\FillPdfFormInterface
+   */
+  protected $entity;
+
+  /**
+   * Returns the question to ask the user.
+   *
+   * @return \Drupal\Core\StringTranslation\TranslatableMarkup
+   *   The form question. The page title will be set to this value.
+   */
+  public function getQuestion() {
+    $label = trim($this->entity->label()) ?: $this->t('unnamed');
+    return $this->t('Create duplicate of %label?', ['%label' => $label]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return $this->t('Save');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelUrl() {
+    return $this->entity->toUrl();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form = parent::buildForm($form, $form_state);
+
+    $label = trim($this->entity->label()) ?: $this->t('unnamed');
+
+    $form['new_admin_title'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Administrative title'),
+      '#required' => TRUE,
+      '#size' => 32,
+      '#maxlength' => 255,
+      '#default_value' => $this->t('Duplicate of @label', ['@label' => $label]),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function actions(array $form, FormStateInterface $form_state) {
+    // @todo: This is a workaround by webform for Core issue #2582295
+    // "Confirmation cancel links are incorrect if installed in a subdirectory".
+    // Remove after a fix landed there.
+    // See: https://www.drupal.org/project/drupal/issues/2582295
+    // See: https://www.drupal.org/project/webform/issues/2899166
+    $request = $this->getRequest();
+    $destination = $request->query->get('destination');
+    if ($destination) {
+      // Remove subdirectory from destination.
+      $update_destination = preg_replace('/^' . preg_quote(base_path(), '/') . '/', '/', $destination);
+      $request->query->set('destination', $update_destination);
+      $actions = parent::actions($form, $form_state);
+      $request->query->set('destination', $destination);
+      return $actions;
+    }
+    else {
+      return parent::actions($form, $form_state);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $new_form = $this->entity->createDuplicate();
+    $new_form->set('admin_title', $form_state->getValue('new_admin_title'));
+    $status = $new_form->save();
+
+    if ($status === SAVED_NEW) {
+      $form_fields = $this->entity->getFormFields();
+      foreach ($form_fields as $fillpdf_form_field) {
+        $duplicate_field = $fillpdf_form_field->createDuplicate();
+        $duplicate_field->set('fillpdf_form', $new_form->id());
+        $duplicate_field->save();
+      }
+
+      $this->getLogger('fillpdf')->notice('Duplicated FillPDF form %original_id to %new_id.', [
+        '%original_id' => $this->entity->id(),
+        '%new_id' => $new_form->id(),
+      ]);
+      $this->messenger()->addStatus($this->t('FillPDF form has been duplicated.'));
+
+      return new RedirectResponse($new_form->toUrl()->toString());
+    }
+  }
+
+}
diff --git a/tests/src/Functional/FillPdfFormDuplicateFormTest.php b/tests/src/Functional/FillPdfFormDuplicateFormTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..6eb38ce8c7a65384aec2cb20b4a5a5d64f147e26
--- /dev/null
+++ b/tests/src/Functional/FillPdfFormDuplicateFormTest.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Drupal\Tests\fillpdf\Functional;
+
+use Drupal\fillpdf\Entity\FillPdfForm;
+use Drupal\fillpdf\Entity\FillPdfFormField;
+use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\fillpdf\Traits\TestFillPdfTrait;
+use Drupal\Core\Url;
+
+/**
+ * @coversDefaultClass \Drupal\fillpdf\Form\FillPdfFormDuplicateForm
+ * @group fillpdf
+ */
+class FillPdfFormDuplicateFormTest extends BrowserTestBase {
+
+  use TestFillPdfTrait;
+
+  static public $modules = ['fillpdf_test'];
+  protected $profile = 'minimal';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->configureFillPdf();
+    $this->initializeUser();
+  }
+
+  /**
+   * Tests the duplicate function.
+   */
+  public function testDuplicateForm() {
+    $this->uploadTestPdf('fillpdf_test_v3.pdf');
+    $form_id = $this->getLatestFillPdfForm();
+    $template_fid = FillPdfForm::load($form_id)->fid->value;
+
+    // Verify the FillPdfForm's fields are stored.
+    $field_ids = \Drupal::entityQuery('fillpdf_form_field')->condition('fillpdf_form', $form_id)->execute();
+    $this->assertCount(4, $field_ids, "4 FillPdfFormFields have been created.");
+
+    // We're now on the edit form. Add an admin title.
+    $this->assertSession()->pageTextContains('New FillPDF form has been created.');
+    $admin_title = 'Test';
+    $this->drupalPostForm(NULL, ['admin_title[0][value]' => $admin_title], 'Save');
+    $this->assertSession()->pageTextContains("FillPDF Form $admin_title has been updated.");
+
+    // Go to the overview form, click duplicate but cancel to come back.
+    $overview_url = Url::fromRoute('fillpdf.forms_admin');
+    $this->drupalGet($overview_url);
+    $this->clickLink('Duplicate');
+    $this->assertSession()->pageTextContains("Create duplicate of $admin_title?");
+    $this->assertSession()->fieldValueEquals('new_admin_title', "Duplicate of $admin_title");
+    $this->clickLink('Cancel');
+    $this->assertSession()->addressEquals($overview_url);
+
+    // Back to the overview form, try again, and this time create a duplicate.
+    $this->clickLink('Duplicate');
+    $this->drupalPostForm(NULL, ['new_admin_title' => 'Another test'], 'Save');
+    $this->assertSession()->pageTextContains('FillPDF form has been duplicated.');
+    $this->assertSession()->addressEquals(Url::fromRoute('fillpdf.forms_admin'));
+
+    // Now verify the FillPdfForm and its fields have actually been duplicated,
+    // but are using the same template file.
+    $new_form_id = $this->getLatestFillPdfForm();
+    $this->assertNotEquals($new_form_id, $form_id);
+    $field_ids = \Drupal::entityQuery('fillpdf_form_field')->condition('fillpdf_form', $new_form_id)->execute();
+    foreach ($field_ids as $id) {
+      $this->assertNotNull(FillPdfFormField::load($id), "The FillPdfFormField #{$id} has ben duplicated.");
+    }
+    $this->assertEquals($template_fid, FillPdfForm::load($form_id)->fid->value);
+  }
+
+}