diff --git a/package_manager/package_manager.services.yml b/package_manager/package_manager.services.yml
index 5e21b987128d0a7ea4c1ddface0922709df33dfc..b6fa99e3ac5a5e8d5ff7deb0248060b7d67956dc 100644
--- a/package_manager/package_manager.services.yml
+++ b/package_manager/package_manager.services.yml
@@ -104,6 +104,12 @@ services:
       - '@package_manager.path_locator'
     tags:
       - { name: event_subscriber }
+  package_manager.validator.duplicate_info_file:
+    class: Drupal\package_manager\Validator\DuplicateInfoFileValidator
+    arguments:
+      - '@package_manager.path_locator'
+    tags:
+      - { name: event_subscriber }
   package_manager.test_site_excluder:
     class: Drupal\package_manager\PathExcluder\TestSiteExcluder
     arguments:
diff --git a/package_manager/src/Validator/DuplicateInfoFileValidator.php b/package_manager/src/Validator/DuplicateInfoFileValidator.php
new file mode 100644
index 0000000000000000000000000000000000000000..56f9ec94781c121145bb9175a12ef0ba4434e3d3
--- /dev/null
+++ b/package_manager/src/Validator/DuplicateInfoFileValidator.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace Drupal\package_manager\Validator;
+
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\package_manager\Event\PreApplyEvent;
+use Drupal\package_manager\PathLocator;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\Finder\Finder;
+
+/**
+ * Validates the stage does not have duplicate info.yml not present in active.
+ *
+ * @internal
+ *   This is an internal part of Package Manager and may be changed or removed
+ *   at any time without warning. External code should not interact with this
+ *   class.
+ */
+class DuplicateInfoFileValidator implements EventSubscriberInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The path locator service.
+   *
+   * @var \Drupal\package_manager\PathLocator
+   */
+  protected $pathLocator;
+
+  /**
+   * Constructs a DuplicateInfoFileValidator object.
+   *
+   * @param \Drupal\package_manager\PathLocator $path_locator
+   *   The path locator service.
+   */
+  public function __construct(PathLocator $path_locator) {
+    $this->pathLocator = $path_locator;
+  }
+
+  /**
+   * Validates the stage does not have duplicate info.yml not present in active.
+   */
+  public function validateDuplicateInfoFileInStage(PreApplyEvent $event): void {
+    $active_dir = $this->pathLocator->getProjectRoot();
+    $stage_dir = $event->getStage()->getStageDirectory();
+    $active_info_files = $this->findInfoFiles($active_dir);
+    $stage_info_files = $this->findInfoFiles($stage_dir);
+
+    foreach ($stage_info_files as $stage_info_file => $stage_info_count) {
+      if (isset($active_info_files[$stage_info_file])) {
+        // Check if stage directory has more info.yml files matching
+        // $stage_info_file than in the active directory.
+        if ($stage_info_count > $active_info_files[$stage_info_file]) {
+          $event->addError([
+            $this->t('The staging directory has @stage_count instances of @stage_info_file as compared to @active_count in the active directory. This likely indicates that a duplicate extension was installed.', [
+              '@stage_info_file' => $stage_info_file,
+              '@stage_count' => $stage_info_count,
+              '@active_count' => $active_info_files[$stage_info_file],
+            ]),
+          ]);
+        }
+      }
+      // Check if stage directory has two or more info.yml files matching
+      // $stage_info_file which are not in active directory.
+      elseif ($stage_info_count > 1) {
+        $event->addError([
+          $this->t('The staging directory has @stage_count instances of @stage_info_file. This likely indicates that a duplicate extension was installed.', [
+            '@stage_info_file' => $stage_info_file,
+            '@stage_count' => $stage_info_count,
+          ]),
+        ]);
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    return [
+      PreApplyEvent::class => 'validateDuplicateInfoFileInStage',
+    ];
+  }
+
+  /**
+   * Recursively finds info.yml files in a directory.
+   *
+   * @param string $dir
+   *   The path of the directory to check.
+   *
+   * @return int[]
+   *   Array of count of info.yml files in the directory keyed by file name.
+   */
+  protected function findInfoFiles(string $dir): array {
+    $info_files_finder = Finder::create()
+      ->in($dir)
+      ->ignoreUnreadableDirs()
+      ->name('*.info.yml');
+    $info_files = [];
+    /** @var \Symfony\Component\Finder\SplFileInfo $info_file */
+    foreach (iterator_to_array($info_files_finder) as $info_file) {
+      // Skipping info.yml files in tests/fixtures because Drupal will not scan
+      // these directories when doing extension discovery.
+      //
+      // @todo We should also skip info.yml files in tests/modules,
+      //   tests/themes, and tests/profiles directories in
+      //   https://www.drupal.org/i/3306163.
+      if (strpos($info_file->getPath(), DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'fixtures') !== FALSE) {
+        continue;
+      }
+      $file_name = $info_file->getFilename();
+      $info_files[$file_name] = ($info_files[$file_name] ?? 0) + 1;
+    }
+    return $info_files;
+  }
+
+}
diff --git a/package_manager/tests/src/Kernel/DuplicateInfoFileValidatorTest.php b/package_manager/tests/src/Kernel/DuplicateInfoFileValidatorTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..da3e3407ff7011d307e0dcbfe4064ce8c846c006
--- /dev/null
+++ b/package_manager/tests/src/Kernel/DuplicateInfoFileValidatorTest.php
@@ -0,0 +1,210 @@
+<?php
+
+namespace Drupal\Tests\package_manager\Kernel;
+
+use Drupal\package_manager\Exception\StageValidationException;
+use Drupal\package_manager\ValidationResult;
+use Symfony\Component\Filesystem\Filesystem;
+
+/**
+ * @covers \Drupal\package_manager\Validator\DuplicateInfoFileValidator
+ *
+ * @group package_manager
+ */
+class DuplicateInfoFileValidatorTest extends PackageManagerKernelTestBase {
+
+  /**
+   * Data provider for testDuplicateInfoFilesInStage.
+   *
+   * @return mixed[][]
+   *   The test cases.
+   */
+  public function providerDuplicateInfoFilesInStage(): array {
+    return [
+      'Duplicate info.yml files in stage' => [
+        [
+          '/module.info.yml',
+        ],
+        [
+          '/module.info.yml',
+          '/modules/module.info.yml',
+        ],
+        [
+          ValidationResult::createError([
+            'The staging directory has 2 instances of module.info.yml as compared to 1 in the active directory. This likely indicates that a duplicate extension was installed.',
+          ]),
+        ],
+      ],
+      // Duplicate files in stage but having different extension which we don't
+      // care about.
+      'Duplicate info files in stage' => [
+        [
+          '/my_file.info',
+        ],
+        [
+          '/my_file.info',
+          '/modules/my_file.info',
+        ],
+        [],
+      ],
+      'Duplicate info.yml files in stage with one file in tests folder' => [
+        [
+          '/tests/fixtures/module.info.yml',
+        ],
+        [
+          '/tests/fixtures/module.info.yml',
+          '/modules/module.info.yml',
+        ],
+        [],
+      ],
+      'Duplicate info.yml files in stage not present in active' => [
+        [],
+        [
+          '/module.info.yml',
+          '/modules/module.info.yml',
+        ],
+        [
+          ValidationResult::createError([
+            'The staging directory has 2 instances of module.info.yml. This likely indicates that a duplicate extension was installed.',
+          ]),
+        ],
+      ],
+      'Duplicate info.yml files in active' => [
+        [
+          '/module.info.yml',
+          '/modules/module.info.yml',
+        ],
+        [
+          '/module.info.yml',
+        ],
+        [],
+      ],
+      'Same number of info.yml files in active and stage' => [
+        [
+          '/module.info.yml',
+          '/modules/module.info.yml',
+        ],
+        [
+          '/module.info.yml',
+          '/modules/module.info.yml',
+        ],
+        [],
+      ],
+      'Multiple duplicate info.yml files in stage' => [
+        [
+          '/module1.info.yml',
+          '/module2.info.yml',
+        ],
+        [
+          '/module1.info.yml',
+          '/modules/module1.info.yml',
+          '/module2.info.yml',
+          '/modules/module2.info.yml',
+          '/module2/module2.info.yml',
+        ],
+        [
+          ValidationResult::createError([
+            'The staging directory has 2 instances of module1.info.yml as compared to 1 in the active directory. This likely indicates that a duplicate extension was installed.',
+          ]),
+          ValidationResult::createError([
+            'The staging directory has 3 instances of module2.info.yml as compared to 1 in the active directory. This likely indicates that a duplicate extension was installed.',
+          ]),
+        ],
+      ],
+      'Multiple duplicate info.yml files in stage not present in active' => [
+        [],
+        [
+          '/module1.info.yml',
+          '/modules/module1.info.yml',
+          '/module2.info.yml',
+          '/modules/module2.info.yml',
+          '/module2/module2.info.yml',
+        ],
+        [
+          ValidationResult::createError([
+            'The staging directory has 2 instances of module1.info.yml. This likely indicates that a duplicate extension was installed.',
+          ]),
+          ValidationResult::createError([
+            'The staging directory has 3 instances of module2.info.yml. This likely indicates that a duplicate extension was installed.',
+          ]),
+        ],
+      ],
+      'Multiple duplicate info.yml files in stage with one info.yml file not present in active' => [
+        [
+          '/module1.info.yml',
+        ],
+        [
+          '/module1.info.yml',
+          '/modules/module1.info.yml',
+          '/module2.info.yml',
+          '/modules/module2.info.yml',
+          '/module2/module2.info.yml',
+        ],
+        [
+          ValidationResult::createError([
+            'The staging directory has 2 instances of module1.info.yml as compared to 1 in the active directory. This likely indicates that a duplicate extension was installed.',
+          ]),
+          ValidationResult::createError([
+            'The staging directory has 3 instances of module2.info.yml. This likely indicates that a duplicate extension was installed.',
+          ]),
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Tests that duplicate info.yml in stage raise an error.
+   *
+   * @param string[] $active_info_files
+   *   An array of info.yml files in active directory.
+   * @param string[] $stage_info_files
+   *   An array of info.yml files in stage directory.
+   * @param \Drupal\package_manager\ValidationResult[] $expected_results
+   *   An array of expected results.
+   *
+   * @dataProvider providerDuplicateInfoFilesInStage
+   */
+  public function testDuplicateInfoFilesInStage(array $active_info_files, array $stage_info_files, array $expected_results): void {
+    $stage = $this->createStage();
+    $stage->create();
+    $stage->require(['composer/semver:^3']);
+
+    $active_dir = $this->container->get('package_manager.path_locator')
+      ->getProjectRoot();
+    $stage_dir = $stage->getStageDirectory();
+    foreach ($active_info_files as $active_info_file) {
+      $this->createFileAtPath($active_dir, $active_info_file);
+    }
+    foreach ($stage_info_files as $stage_info_file) {
+      $this->createFileAtPath($stage_dir, $stage_info_file);
+    }
+    try {
+      $stage->apply();
+      $this->assertEmpty($expected_results);
+    }
+    catch (StageValidationException $e) {
+      $this->assertNotEmpty($expected_results);
+      $this->assertValidationResultsEqual($expected_results, $e->getResults());
+    }
+  }
+
+  /**
+   * Creates the file at the root directory.
+   *
+   * @param string $root_directory
+   *   The base directory in which the file will be created.
+   * @param string $file_path
+   *   The path of the file to create.
+   */
+  private function createFileAtPath(string $root_directory, string $file_path): void {
+    $parts = explode(DIRECTORY_SEPARATOR, $file_path);
+    $filename = array_pop($parts);
+    $file_dir = str_replace($filename, '', $file_path);
+    $fs = new Filesystem();
+    if (!file_exists($file_dir)) {
+      $fs->mkdir($root_directory . $file_dir);
+    }
+    file_put_contents($root_directory . $file_path, ' ');
+  }
+
+}