diff --git a/docs/init.md b/docs/init.md
index 59a936366..45877e32c 100644
--- a/docs/init.md
+++ b/docs/init.md
@@ -73,6 +73,8 @@ module.exports = {
 
   // Path to script, which will be executed after initialization process, but before installing all the dependencies specified in the template. This script runs as a shell script but you can change that (e.g. to Node) by using a shebang (see example custom template).
   postInitScript: './script.js',
+  // We're also using `template.config.js` when adding new platforms to existing project in `add-platform` command. Thanks to value passed to `platformName` we know which folder we should copy to the project.
+  platformName: 'visionos',
 };
 ```
 
@@ -91,12 +93,16 @@ new Promise((resolve) => {
   spinner.start();
   // do something
   resolve();
-}).then(() => {
-  spinner.succeed();
-}).catch(() => {
-  spinner.fail();
-  throw new Error('Something went wrong during the post init script execution');
-});
+})
+  .then(() => {
+    spinner.succeed();
+  })
+  .catch(() => {
+    spinner.fail();
+    throw new Error(
+      'Something went wrong during the post init script execution',
+    );
+  });
 ```
 
 You can find example custom template [here](https://github.com/Esemesek/react-native-new-template).
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 24d15f165..67ce20824 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -38,6 +38,7 @@
     "find-up": "^5.0.0",
     "fs-extra": "^8.1.0",
     "graceful-fs": "^4.1.3",
+    "npm-registry-fetch": "^16.1.0",
     "prompts": "^2.4.2",
     "semver": "^7.5.2"
   },
@@ -45,6 +46,7 @@
     "@types/fs-extra": "^8.1.0",
     "@types/graceful-fs": "^4.1.3",
     "@types/hapi__joi": "^17.1.6",
+    "@types/npm-registry-fetch": "^8.0.7",
     "@types/prompts": "^2.4.4",
     "@types/semver": "^6.0.2",
     "slash": "^3.0.0",
diff --git a/packages/cli/src/commands/addPlatform/addPlatform.ts b/packages/cli/src/commands/addPlatform/addPlatform.ts
new file mode 100644
index 000000000..5488e3640
--- /dev/null
+++ b/packages/cli/src/commands/addPlatform/addPlatform.ts
@@ -0,0 +1,262 @@
+import {
+  CLIError,
+  getLoader,
+  logger,
+  prompt,
+} from '@react-native-community/cli-tools';
+import {Config} from '@react-native-community/cli-types';
+import {join} from 'path';
+import {readFileSync} from 'fs';
+import chalk from 'chalk';
+import {install, PackageManager} from './../../tools/packageManager';
+import npmFetch from 'npm-registry-fetch';
+import semver from 'semver';
+import {checkGitInstallation, isGitTreeDirty} from '../init/git';
+import {changePlaceholderInTemplate} from '../init/editTemplate';
+import {
+  copyTemplate,
+  executePostInitScript,
+  getTemplateConfig,
+  installTemplatePackage,
+} from '../init/template';
+import {tmpdir} from 'os';
+import {mkdtempSync} from 'graceful-fs';
+import {existsSync} from 'fs';
+import {getNpmRegistryUrl} from '../../tools/npm';
+
+type Options = {
+  packageName: string;
+  version: string;
+  pm: PackageManager;
+  title: string;
+};
+
+const NPM_REGISTRY_URL = getNpmRegistryUrl();
+
+const getAppName = async (root: string) => {
+  logger.log(`Reading ${chalk.cyan('name')} from package.json…`);
+  const pkgJsonPath = join(root, 'package.json');
+
+  if (!pkgJsonPath) {
+    throw new CLIError(`Unable to find package.json inside ${root}`);
+  }
+
+  let name;
+
+  try {
+    name = JSON.parse(readFileSync(pkgJsonPath, 'utf8')).name;
+  } catch (e) {
+    throw new CLIError(`Failed to read ${pkgJsonPath} file.`, e as Error);
+  }
+
+  if (!name) {
+    const appJson = join(root, 'app.json');
+    if (appJson) {
+      logger.log(`Reading ${chalk.cyan('name')} from app.json…`);
+      try {
+        name = JSON.parse(readFileSync(appJson, 'utf8')).name;
+      } catch (e) {
+        throw new CLIError(`Failed to read ${pkgJsonPath} file.`, e as Error);
+      }
+    }
+
+    if (!name) {
+      throw new CLIError('Please specify name in package.json or app.json.');
+    }
+  }
+
+  return name;
+};
+
+const getPackageMatchingVersion = async (
+  packageName: string,
+  version: string,
+) => {
+  const npmResponse = await npmFetch.json(packageName, {
+    registry: NPM_REGISTRY_URL,
+  });
+
+  if ('dist-tags' in npmResponse) {
+    const distTags = npmResponse['dist-tags'] as Record<string, string>;
+    if (version in distTags) {
+      return distTags[version];
+    }
+  }
+
+  if ('versions' in npmResponse) {
+    const versions = Object.keys(
+      npmResponse.versions as Record<string, unknown>,
+    );
+    if (versions.length > 0) {
+      const candidates = versions
+        .filter((v) => semver.satisfies(v, version))
+        .sort(semver.rcompare);
+
+      if (candidates.length > 0) {
+        return candidates[0];
+      }
+    }
+  }
+
+  throw new Error(
+    `Cannot find matching version of ${packageName} to react-native${version}, please provide version manually with --version flag.`,
+  );
+};
+
+// From React Native 0.75 template is not longer inside `react-native` core,
+// so we need to map package name (fork) to template name
+
+const getTemplateNameFromPackageName = (packageName: string) => {
+  switch (packageName) {
+    case '@callstack/react-native-visionos':
+    case 'react-native-visionos':
+      return '@callstack/visionos-template';
+    default:
+      return packageName;
+  }
+};
+
+async function addPlatform(
+  [packageName]: string[],
+  {root, reactNativeVersion}: Config,
+  {version, pm, title}: Options,
+) {
+  if (!packageName) {
+    throw new CLIError('Please provide package name e.g. react-native-macos');
+  }
+
+  const templateName = getTemplateNameFromPackageName(packageName);
+  const isGitAvailable = await checkGitInstallation();
+
+  if (isGitAvailable) {
+    const dirty = await isGitTreeDirty(root);
+
+    if (dirty) {
+      logger.warn(
+        'Your git tree is dirty. We recommend committing or stashing changes first.',
+      );
+      const {proceed} = await prompt({
+        type: 'confirm',
+        name: 'proceed',
+        message: 'Would you like to proceed?',
+      });
+
+      if (!proceed) {
+        return;
+      }
+
+      logger.info('Proceeding with the installation');
+    }
+  }
+
+  const projectName = await getAppName(root);
+
+  const matchingVersion = await getPackageMatchingVersion(
+    packageName,
+    version ?? reactNativeVersion,
+  );
+
+  logger.log(
+    `Found matching version ${chalk.cyan(matchingVersion)} for ${chalk.cyan(
+      packageName,
+    )}`,
+  );
+
+  const loader = getLoader({
+    text: `Installing ${packageName}@${matchingVersion}`,
+  });
+
+  loader.start();
+
+  try {
+    await install([`${packageName}@${matchingVersion}`], {
+      packageManager: pm,
+      silent: true,
+      root,
+    });
+    loader.succeed();
+  } catch (error) {
+    loader.fail();
+    throw new CLIError(
+      `Failed to install package ${packageName}@${matchingVersion}`,
+      (error as Error).message,
+    );
+  }
+
+  loader.start(
+    `Installing template packages from ${templateName}@0${matchingVersion}`,
+  );
+
+  const templateSourceDir = mkdtempSync(join(tmpdir(), 'rncli-init-template-'));
+
+  try {
+    await installTemplatePackage(
+      `${templateName}@0${matchingVersion}`,
+      templateSourceDir,
+      pm,
+    );
+    loader.succeed();
+  } catch (error) {
+    loader.fail();
+    throw new CLIError(
+      `Failed to install template packages from ${templateName}@0${matchingVersion}`,
+      (error as Error).message,
+    );
+  }
+
+  loader.start('Copying template files');
+
+  const templateConfig = getTemplateConfig(templateName, templateSourceDir);
+
+  if (!templateConfig.platforms) {
+    throw new CLIError(
+      `Template ${templateName} is missing "platforms" in its "template.config.js"`,
+    );
+  }
+
+  for (const platform of templateConfig.platforms) {
+    if (existsSync(join(root, platform))) {
+      loader.fail();
+      throw new CLIError(
+        `Platform ${platform} already exists in the project. Directory ${join(
+          root,
+          platform,
+        )} is not empty.`,
+      );
+    }
+
+    await copyTemplate(
+      templateName,
+      templateConfig.templateDir,
+      templateSourceDir,
+      platform,
+    );
+  }
+
+  loader.succeed();
+  loader.start('Processing template');
+
+  for (const platform of templateConfig.platforms) {
+    await changePlaceholderInTemplate({
+      projectName,
+      projectTitle: title,
+      placeholderName: templateConfig.placeholderName,
+      placeholderTitle: templateConfig.titlePlaceholder,
+      projectPath: join(root, platform),
+    });
+  }
+
+  loader.succeed();
+
+  const {postInitScript} = templateConfig;
+  if (postInitScript) {
+    logger.debug('Executing post init script ');
+    await executePostInitScript(
+      templateName,
+      postInitScript,
+      templateSourceDir,
+    );
+  }
+}
+
+export default addPlatform;
diff --git a/packages/cli/src/commands/addPlatform/index.ts b/packages/cli/src/commands/addPlatform/index.ts
new file mode 100644
index 000000000..c8de652f1
--- /dev/null
+++ b/packages/cli/src/commands/addPlatform/index.ts
@@ -0,0 +1,23 @@
+import addPlatform from './addPlatform';
+
+export default {
+  func: addPlatform,
+  name: 'add-platform [packageName]',
+  description: 'Add new platform to your React Native project.',
+  options: [
+    {
+      name: '--version <string>',
+      description: 'Pass version of the platform to be added to the project.',
+    },
+    {
+      name: '--pm <string>',
+      description:
+        'Use specific package manager to initialize the project. Available options: `yarn`, `npm`, `bun`. Default: `yarn`',
+      default: 'yarn',
+    },
+    {
+      name: '--title <string>',
+      description: 'Uses a custom app title name for application',
+    },
+  ],
+};
diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts
index 306fd5449..6a70b0f27 100644
--- a/packages/cli/src/commands/index.ts
+++ b/packages/cli/src/commands/index.ts
@@ -3,11 +3,13 @@ import {commands as cleanCommands} from '@react-native-community/cli-clean';
 import {commands as doctorCommands} from '@react-native-community/cli-doctor';
 import {commands as configCommands} from '@react-native-community/cli-config';
 import init from './init';
+import addPlatform from './addPlatform';
 
 export const projectCommands = [
   ...configCommands,
   cleanCommands.clean,
   doctorCommands.info,
+  addPlatform,
 ] as Command[];
 
 export const detachedCommands = [
diff --git a/packages/cli/src/commands/init/editTemplate.ts b/packages/cli/src/commands/init/editTemplate.ts
index d3bb39f23..9692a4387 100644
--- a/packages/cli/src/commands/init/editTemplate.ts
+++ b/packages/cli/src/commands/init/editTemplate.ts
@@ -13,6 +13,7 @@ interface PlaceholderConfig {
   placeholderTitle?: string;
   projectTitle?: string;
   packageName?: string;
+  projectPath?: string;
 }
 
 /**
@@ -145,11 +146,12 @@ export async function replacePlaceholderWithPackageName({
   placeholderName,
   placeholderTitle,
   packageName,
+  projectPath = process.cwd(),
 }: Omit<Required<PlaceholderConfig>, 'projectTitle'>) {
   validatePackageName(packageName);
   const cleanPackageName = packageName.replace(/[^\p{L}\p{N}.]+/gu, '');
 
-  for (const filePath of walk(process.cwd()).reverse()) {
+  for (const filePath of walk(projectPath).reverse()) {
     if (shouldIgnoreFile(filePath)) {
       continue;
     }
@@ -232,6 +234,7 @@ export async function changePlaceholderInTemplate({
   placeholderTitle = DEFAULT_TITLE_PLACEHOLDER,
   projectTitle = projectName,
   packageName,
+  projectPath = process.cwd(),
 }: PlaceholderConfig) {
   logger.debug(`Changing ${placeholderName} for ${projectName} in template`);
 
@@ -242,12 +245,13 @@ export async function changePlaceholderInTemplate({
         placeholderName,
         placeholderTitle,
         packageName,
+        projectPath,
       });
     } catch (error) {
       throw new CLIError((error as Error).message);
     }
   } else {
-    for (const filePath of walk(process.cwd()).reverse()) {
+    for (const filePath of walk(projectPath).reverse()) {
       if (shouldIgnoreFile(filePath)) {
         continue;
       }
@@ -269,3 +273,22 @@ export async function changePlaceholderInTemplate({
     }
   }
 }
+
+export function getTemplateName(cwd: string) {
+  // We use package manager to infer the name of the template module for us.
+  // That's why we get it from temporary package.json, where the name is the
+  // first and only dependency (hence 0).
+  let name;
+  try {
+    name = Object.keys(
+      JSON.parse(fs.readFileSync(path.join(cwd, './package.json'), 'utf8'))
+        .dependencies,
+    )[0];
+  } catch {
+    throw new CLIError(
+      'Failed to read template name from package.json. Please make sure that the template you are using has a valid package.json file.',
+    );
+  }
+
+  return name;
+}
diff --git a/packages/cli/src/commands/init/git.ts b/packages/cli/src/commands/init/git.ts
index 10eee2411..00058dde9 100644
--- a/packages/cli/src/commands/init/git.ts
+++ b/packages/cli/src/commands/init/git.ts
@@ -68,3 +68,14 @@ export const createGitRepository = async (folder: string) => {
     );
   }
 };
+
+export const isGitTreeDirty = async (folder: string) => {
+  try {
+    const {stdout} = await execa('git', ['status', '--porcelain'], {
+      cwd: folder,
+    });
+    return stdout !== '';
+  } catch {
+    return false;
+  }
+};
diff --git a/packages/cli/src/commands/init/init.ts b/packages/cli/src/commands/init/init.ts
index f4da03370..e906945ea 100644
--- a/packages/cli/src/commands/init/init.ts
+++ b/packages/cli/src/commands/init/init.ts
@@ -19,7 +19,7 @@ import {
   copyTemplate,
   executePostInitScript,
 } from './template';
-import {changePlaceholderInTemplate} from './editTemplate';
+import {changePlaceholderInTemplate, getTemplateName} from './editTemplate';
 import * as PackageManager from '../../tools/packageManager';
 import banner from './banner';
 import TemplateAndVersionError from './errors/TemplateAndVersionError';
@@ -181,17 +181,6 @@ async function setProjectDirectory(
   return process.cwd();
 }
 
-function getTemplateName(cwd: string) {
-  // We use package manager to infer the name of the template module for us.
-  // That's why we get it from temporary package.json, where the name is the
-  // first and only dependency (hence 0).
-  const name = Object.keys(
-    JSON.parse(fs.readFileSync(path.join(cwd, './package.json'), 'utf8'))
-      .dependencies,
-  )[0];
-  return name;
-}
-
 //set cache to empty string to prevent installing cocoapods on freshly created project
 function setEmptyHashForCachedDependencies(projectName: string) {
   cacheManager.set(
diff --git a/packages/cli/src/commands/init/template.ts b/packages/cli/src/commands/init/template.ts
index ee2adb537..1fd39ad16 100644
--- a/packages/cli/src/commands/init/template.ts
+++ b/packages/cli/src/commands/init/template.ts
@@ -1,5 +1,5 @@
 import execa from 'execa';
-import path from 'path';
+import path, {join} from 'path';
 import {logger, CLIError} from '@react-native-community/cli-tools';
 import * as PackageManager from '../../tools/packageManager';
 import copyFiles from '../../tools/copyFiles';
@@ -14,6 +14,7 @@ export type TemplateConfig = {
   templateDir: string;
   postInitScript?: string;
   titlePlaceholder?: string;
+  platforms?: string[];
 };
 
 export async function installTemplatePackage(
@@ -91,6 +92,7 @@ export async function copyTemplate(
   templateName: string,
   templateDir: string,
   templateSourceDir: string,
+  platform: string = '',
 ) {
   const templatePath = path.resolve(
     templateSourceDir,
@@ -101,7 +103,7 @@ export async function copyTemplate(
 
   logger.debug(`Copying template from ${templatePath}`);
   let regexStr = path.resolve(templatePath, 'node_modules');
-  await copyFiles(templatePath, process.cwd(), {
+  await copyFiles(join(templatePath, platform), join(process.cwd(), platform), {
     exclude: [new RegExp(replacePathSepForRegex(regexStr))],
   });
 }